libobs_wrapper\data\output/
pipeline.rs1use std::sync::Arc;
8
9use crate::{
10 capabilities::{OutputCapabilities, OutputTypeInfo},
11 context::ObsContext,
12 data::{object::ObsObjectTrait, ObsData, ObsDataPointers},
13 encoders::{audio::ObsAudioEncoder, video::ObsVideoEncoder, ObsEncoderTrait},
14 services::ObsServiceRef,
15 utils::{ObsError, ObsString},
16};
17
18use super::{ObsOutputComposition, ObsOutputRef, ObsOutputTrait};
19
20#[derive(Debug)]
22pub struct ObsOutputPipelineBuilder {
23 context: ObsContext,
24 output_type: OutputTypeInfo,
25 name: ObsString,
26 settings: Option<ObsData>,
27 composition: ObsOutputComposition,
28}
29
30impl ObsOutputPipelineBuilder {
31 pub(crate) fn new(
32 context: ObsContext,
33 output_type: OutputTypeInfo,
34 name: ObsString,
35 settings: Option<ObsData>,
36 ) -> Self {
37 Self {
38 context,
39 output_type,
40 name,
41 settings,
42 composition: ObsOutputComposition::new(),
43 }
44 }
45
46 pub fn video_encoder(mut self, encoder: Arc<ObsVideoEncoder>) -> Self {
47 self.composition = self.composition.with_video_encoder(encoder);
48 self
49 }
50
51 pub fn audio_encoder(mut self, mixer_idx: usize, encoder: Arc<ObsAudioEncoder>) -> Self {
52 self.composition = self.composition.with_audio_encoder(mixer_idx, encoder);
53 self
54 }
55
56 pub fn service(mut self, service: Arc<ObsServiceRef>) -> Self {
57 self.composition = self.composition.with_service(service);
58 self
59 }
60
61 pub fn validate(&self) -> Result<(), ObsError> {
63 self.context
64 .runtime()
65 .ensure_same_runtime(self.output_type.runtime())?;
66
67 if let Some(settings) = self.settings.as_ref() {
68 self.context
69 .runtime()
70 .ensure_same_runtime(settings.runtime())?;
71 }
72
73 let flags = self.output_type.capability_flags();
74 let encoded = flags.contains(OutputCapabilities::ENCODED);
75 let needs_video = encoded && flags.contains(OutputCapabilities::VIDEO);
76 let needs_audio = encoded && flags.contains(OutputCapabilities::AUDIO);
77 let needs_service = flags.contains(OutputCapabilities::SERVICE);
78
79 let video_encoder = self.composition.video_encoder();
80 if needs_video && video_encoder.is_none() {
81 return Err(missing_component(&self.output_type, "a video encoder"));
82 }
83 if video_encoder.is_some() && !needs_video {
84 return Err(unexpected_component(&self.output_type, "a video encoder"));
85 }
86
87 let audio_encoders = self.composition.audio_encoders();
88 if needs_audio && audio_encoders.is_empty() {
89 return Err(missing_component(
90 &self.output_type,
91 "at least one audio encoder",
92 ));
93 }
94 if !audio_encoders.is_empty() && !needs_audio {
95 return Err(unexpected_component(&self.output_type, "audio encoders"));
96 }
97 for (mixer_idx, encoder) in audio_encoders {
98 if *mixer_idx >= libobs::MAX_AUDIO_MIXES as usize {
99 return Err(ObsError::AudioMixerIndexOutOfBounds {
100 index: *mixer_idx,
101 max: (libobs::MAX_AUDIO_MIXES - 1) as usize,
102 });
103 }
104 self.context
105 .runtime()
106 .ensure_same_runtime(encoder.runtime())?;
107 }
108
109 let service = self.composition.service();
110 if needs_service && service.is_none() {
111 return Err(missing_component(&self.output_type, "a service"));
112 }
113 if service.is_some() && !needs_service {
114 return Err(unexpected_component(&self.output_type, "a service"));
115 }
116
117 if let Some(encoder) = video_encoder {
118 self.context
119 .runtime()
120 .ensure_same_runtime(encoder.runtime())?;
121 if let Some(codec) = encoder.codec()? {
122 if !self.output_type.video_codecs().is_empty()
123 && !self.output_type.supports_video_codec(&codec)
124 {
125 return Err(ObsError::OutputPipelineUnsupportedCodec {
126 output_id: self.output_type.id().to_owned(),
127 media: "video".to_owned(),
128 codec,
129 });
130 }
131 }
132 }
133
134 for encoder in audio_encoders.values() {
135 if let Some(codec) = encoder.codec()? {
136 if !self.output_type.audio_codecs().is_empty()
137 && !self.output_type.supports_audio_codec(&codec)
138 {
139 return Err(ObsError::OutputPipelineUnsupportedCodec {
140 output_id: self.output_type.id().to_owned(),
141 media: "audio".to_owned(),
142 codec,
143 });
144 }
145 }
146 }
147
148 if let Some(service) = service {
149 self.context
150 .runtime()
151 .ensure_same_runtime(service.runtime())?;
152 if let Some(protocol) = service.protocol()? {
153 if !self.output_type.protocols().is_empty()
154 && !self.output_type.supports_protocol(&protocol)
155 {
156 return Err(ObsError::OutputPipelineUnsupportedProtocol {
157 output_id: self.output_type.id().to_owned(),
158 protocol,
159 });
160 }
161 }
162 }
163
164 Ok(())
165 }
166
167 pub fn build(mut self) -> Result<ObsOutputPipeline, ObsError> {
170 self.validate()?;
171 let output =
172 self.context
173 .create_output(&self.output_type, self.name, self.settings.take())?;
174 output.apply_composition(self.composition)?;
175 Ok(ObsOutputPipeline { output })
176 }
177}
178
179#[derive(Clone, Debug)]
181pub struct ObsOutputPipeline {
182 output: ObsOutputRef,
183}
184
185impl ObsOutputPipeline {
186 pub fn output(&self) -> &ObsOutputRef {
187 &self.output
188 }
189
190 pub fn start(&self) -> Result<(), ObsError> {
191 self.output.start()
192 }
193
194 pub fn stop(&self) -> Result<(), ObsError> {
195 self.output.stop()
196 }
197
198 pub fn is_active(&self) -> Result<bool, ObsError> {
199 self.output.is_active()
200 }
201
202 pub fn into_output(self) -> ObsOutputRef {
203 self.output
204 }
205}
206
207impl ObsContext {
208 pub fn output_pipeline(
210 &self,
211 output_type: &OutputTypeInfo,
212 name: impl Into<ObsString>,
213 settings: Option<ObsData>,
214 ) -> ObsOutputPipelineBuilder {
215 ObsOutputPipelineBuilder::new(self.clone(), output_type.clone(), name.into(), settings)
216 }
217}
218
219fn missing_component(output_type: &OutputTypeInfo, component: &str) -> ObsError {
220 ObsError::OutputPipelineMissingComponent {
221 output_id: output_type.id().to_owned(),
222 component: component.to_owned(),
223 }
224}
225
226fn unexpected_component(output_type: &OutputTypeInfo, component: &str) -> ObsError {
227 ObsError::OutputPipelineUnexpectedComponent {
228 output_id: output_type.id().to_owned(),
229 component: component.to_owned(),
230 }
231}